使用 decimal 代替 float
使用 decimal 代替 float
Decimal 和 Float 或者 Double 类型一样,可以用来表示实数
相比于另外两种类型,Decimal 有下列优点:
- 它的计算方式和人类所学的计算方式一样
- 精确表达
- 在计算和比较中精度不会丢失
- Decimal在计算和输出的时候不会丢失尾随0
- 可以改变精度设置,而不是和 float 或者 double 一样,精度是和语言实现或者硬件相关
数据库使用 decimal
mysql
DDL 定义 DECIMAL,包含两部分精度(p, d)
p 代表支持的总的数值长度精度(1~65)
d 代表支持的小数点后精度(0~30)
考虑遵守公认会计原则(GAAP)规则,货币栏必须至少包含4
位小数,我们在使用时可以设置p为12,d为4。
mongodb
MongoDB 3.4 新增对decimal128 format的支持,最多支持34位小数位。腾讯云的版本为3.2,所以目前我们在mongo这边不支持。
可以考虑使用字符串格式代替,计算的时候转化成decimal。
SqlAlchemy
SqlAlchemy使用Numeric这个类型来表示Decimal
>>> class SKU(Base):
>>> __tablename__ = 'sku'
>>>
>>> id = Column(Integer, primary_key=True)
>>> name = Column(String(32))
>>> price = Column(Numeric(12, 4))
>>> Session.add(SKU(name='test1', price=Decimal('10.030')))
>>> Session.commit()
>>> sku = Session.query(SKU).first()
>>> sku.price
Decimal('10.0300') # 可以看到这个输出有4位小数精度,和我们在 Scheme 内定义的一致
Python
我们在数据库内使用 Decimal 存储数据,那么为了不丢失精度,在代码中也需要在整个过程中使用 Decimal。
输入, param_check 应该加一个 decimal 字段的检查
输出,返回前端的数据将 decimal 转化成 str 输出
计算
Python 的 decimal模块主要由三部分构成:the decimal number ,the context of arithmetic ,signals 。
- decimal number是不可改变的常量,它也不会截取小数点后多余的0;除了正常的数外, 它还包括'Infinity','-Infinity','NaN'等数。
- the context of arithmetic是当前计算环境的一些参数,包括精度位数prec,舍弃位数规则rounding,指数的最大值最小值Emin、Emax,科学计数法e的大小写Capitals,指数是否超出范围clamped,运算结果的标志flags,哪些操作要触发traps等。
- signals是在运算过程中产生的一些状态,这些状态可以根据需要用来提示、忽略、报错等。signals 被触发会出现在 flags 内。
给一个例子简单说明一下context里面flags和trpas的作用
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> Decimal('1.3000')/0
---------------------------------------------------------------------------
DivisionByZero Traceback (most recent call last)
<ipython-input-23-13484ddb1fdf> in <module>()
----> 1 Decimal('1.3000')/0
DivisionByZero: [<class 'decimal.DivisionByZero'>]
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[DivisionByZero], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> getcontext().traps[DivisionByZero]=False
>>> Decimal('1.3000')/0
Decimal('Infinity') # 返回的结果是正无穷
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[DivisionByZero], traps=[InvalidOperation, Overflow])
可以看到traps负责控制哪些异常需要抛出,而flags则记录了计算中出现的异常
值得一提的是flags是不会在计算结束之后自动清空的,他会记录曾经出现过的所有异常,如果你需要监控调试计算,需要手动使用clear_flags
清空flags
我们在使用 Decimal 的时候应该在工程全局设置 context 以保证所有地方的异常和边界计算方法都是一致的。当然有特殊需求也可以利用 with 语句在局部调整设置。
decimal模块中提供了10种signals,下面简单介绍一下:
1)Clamped:越界,指数超出Emin或Emax范围;如果发生,则会在小数部分添加0来表示;
2)DecimalException;异常基类,我们不会用到。
3)DivisionByZero:在除法运算中出现,除数为0;如果不捕捉该错误,则返回Infinity或-Infinity;
4)Inexact:不精确,使用round函数舍弃的小数部分中包含除0以外的数字;
5)InvalidOperation:无效计算或计算无意义,比如两个无穷大相减等;如果不捕捉该错误,则返回NaN(Not a Number);
6)Overflow:在round后指数超出Emax范围,如果不捕捉,则根据round规则来判断返回什么值;
7)Rounded:如果round操作舍弃了小数,不管是不是0,都发生;如果不捕捉,则返回 值未改变;
8)Subnormal:指数值过小;如果不捕捉,则返回 值不变;
9)Underflow:指数值太小,且round操作向0逼近;
10)FloatOperation:如果不捕捉,则混合float型和Decimal型的操作可以执行;如果捕捉,则只有相等判断和显式转换可以执行,其余的都报错。
关于舍入的方法,Decimal中有以下几种类型:
1)ROUND_UP:舍弃小数部分非0时,在前面增加数字,如 5.21 -> 5.3;
2)ROUND_DOWN:舍弃小数部分,从不在前面数字做增加操作,如5.21->5.2;
3)ROUND_CEILING:如果Decimal为正,则做ROUND_UP操作;如果Decimal为负,则做ROUND_DOWN操作;
4)ROUND_FLOOR:如果Decimal为负,则做ROUND_UP操作;如果Decimal为正,则做ROUND_DOWN操作;
5)ROUND_HALF_DOWN:如果舍弃部分>.5,则做ROUND_UP操作;否则,做ROUND_DOWN操作;
6)ROUND_HALF_UP:如果舍弃部分>=.5,则做ROUND_UP操作;否则,做ROUND_DOWN操作,就是一般定义下的四舍五入;
7)ROUND_HALF_EVEN:如果舍弃部分左边的数字是奇数,则做ROUND_HALF_UP操作;若为偶数,则做ROUND_HALF_DOWN操作,就是默认的round行为,所谓的银行家算法;
我们可以将round行为全部调成ROUND_HALF_UP与一般人认知保持一致。
一些值得一说的性质:
-
Decimal 不能直接和 float 进行计算,需要手动把 float 格式的数转化成 Decimal 才可计算。
-
Decimal 不是完全不会损失精度的,它只能保证在设定的精度范围内不会丢失精度,超出部分会做
round
处理,所以我们使用时应该设置好我们需要的最小精度范围。 -
默认设置下 Decimal 接受 float 值的传参,然后根据精度设置做round处理,如果不愿意利用这个特性,可以通过设置 traps 在传入 float 时抛出异常,这样有助于保证数据的绝对正确。
-
在不同的 context 中,相同的计算公式得出的结果很可能是不等的,因为根据不同的精度设置有可能得到不同的结果,所以非必要情况下,不要切换 context
-
在精度有限的条件下,计算的先后顺序也可能导致结果的不同:
>>> getcontext().prec = 8
>>> u, v, w = Decimal(11111113), Decimal(-11111111), Decimal('7.51111111')
>>> (u + v) + w
Decimal('9.5111111')
>>> u + (v + w)
Decimal('10')
>>> u, v, w = Decimal(20000), Decimal(-6), Decimal('6.0000003')
>>> (u*v) + (u*w)
Decimal('0.01')
>>> u * (v+w)
Decimal('0.0060000')
- 特殊的 Decimal 值计算的结果
Decimal 中 有 NaN
, sNaN
, -Infinity
, Infinity
,, +0
和 -0
.这几个不同寻常的值。
Infinity
在不抛出DivisionByZero
的情况下可以通过非0的值处以0得到
NaN
在不抛出InvalidOperation
的情况下可以通过0/0
或者Infinity/Infinity
得到
NaN
和包括自己的任何值做除了不等以外的任何比较,结果都是False。
A variant is sNaN
which signals rather than remaining quiet after every operation. This is a useful return value when an invalid result needs to interrupt a calculation for special handling. 这个我没看懂。
如果不愿意在代码里抛出相关异常,这些值在计算中就很有用了。
-
可以利用
import decimal.Decimal as D
来简化代码 -
利用 signals 里的
Inexact
可以有效检查计算过程中生成的不精确值。 -
quantize
方法用于保持计算过程中的有效数字保留,可以在这个方法内指定Inexact
来做校验,避免精度丢失
```
a = Decimal('102.72') b = Decimal('3.17') (a * b).quantize(TWOPLACES) # Must quantize non-integer multiplication Decimal('325.62') (b / a).quantize(TWOPLACES) # And quantize division Decimal('0.03')
Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact])) Traceback (most recent call last): ... Inexact: None ```
-
在初始化时,Decimal 是不会根据 context 中设置的精度丢失精度的,除非超出了硬件限制,抛出异常,可以利用一元操作符强制做 round 处理
```
getcontext().prec = 3 Decimal('1.23456789') Decimal('1.23456789')
+Decimal('1.23456789') # unary plus triggers rounding Decimal('1.23') ```
-
getcontext()
方法访问每个线程不同的 context 对象,setcontext()
有相同的行为模式,它只会影响当前线程的context值。如果getcontext
在setcontext
前调用,会从DefaultContext
复制一份出来,所以如果要保持全局设置一致,直接改DefaultContext
就好。